로딩 중이에요... 🐣
01 kakaoMap data | ✅ 저자: 이유정(박사)
카카오맵https://map.kakao.com/ Selenium 공식 문서 (Python)https://www.selenium.dev/documentation/webdriver/
webdriver-manager 공식 문서 https://pypi.org/project/webdriver-manager/
폴더 및 파일생성
공식문서에 있는 기본 뼈대 탬플릿은 다음과 같습니다:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
def crawl_example():
driver = None
try:
# 1. 드라이버 셋업
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
# 2. 페이지 열기
driver.get("https://example.com")
# 3. 요소 로딩 대기
wait = WebDriverWait(driver, 10)
target = wait.until(ec.presence_of_element_located((By.ID, "some-id")))
# 4. 입력 및 동작
target.send_keys("검색어")
# 5. 결과 처리
result = driver.page_source
return result
finally:
driver.quit()
kakaomap_scrap1.py
코드작성
Selenium 기반 크롤링
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
def get_data_from_kakaomap():
try:
# 초기 셋업 (웹드라이버 설정)
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
# 호출부 (웹사이트 접속 및 초기 로딩 대기)
driver.get("https://map.kakao.com/")
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "search.keyword.query")))
# 입력부 (검색어 입력 및 실행)
search_input = driver.find_element(By.ID, "search.keyword.query")
search_input.send_keys("강남구 카페")
search_input.send_keys(Keys.ENTER)
# 대기부 (검색 결과 리스트 로딩 대기)
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
# 출력부 (HTML 추출)
place_list = driver.find_element(By.ID, "info.search.place.list")
shop_list = place_list.get_attribute("innerHTML")
# 종료부 (브라우저 닫기 + 반환)
driver.quit()
return shop_list
except Exception as e:
print(e)
raise e
위 코드는 웹드라이버 크롤링의 기본 구조 (템플릿)입니다. 즉, 웹드라이버로 특정 페이지에서 원하는 데이터를 가져오는 자동화 스크립트의 골격이라고 생각하면 됩니다.
# 입력부 (검색어 입력 및 실행)
search_input = driver.find_element(By.ID, "search.keyword.query")
search_input.send_keys("강남구 카페")
search_input.send_keys(Keys.ENTER)
By.ID
는 HTML 요소의id
속성을 기준으로 요소를 찾는 방식입니다."search.keyword.query"
는 해당 입력 필드의id
값입니다.- HTML 문서 내에서 id='search.keyword.query'를 가진
<input>
요소를 찾기
카카오맵이 검색하는 과정을 살펴보면 input태그에서 검색어를 입력하고 엔터 또는 돋보기를 클릭한다.
find_element(by, value)
:WebDriver
또는WebElement
객체의 내장 메서드로 웹 페이지에서 단일 요소를 찾기 위한 메서드입니다.By.ID
,By.CLASS_NAME
,By.XPATH
,By.CSS_SELECTOR
: 매개변수로 어떤 기준으로 요소를 찾을지 지정하는 Enum 객체value
: 찾을 대상의 실제 식별값 (id, class명, XPath 표현식 등)
특수키종류
from selenium.webdriver.common.keys import Keys
Keys.ENTER # 엔터
Keys.RETURN # 리턴 (ENTER와 동일한 경우도 있음)
Keys.TAB # 탭 키
Keys.ESCAPE # ESC
Keys.BACKSPACE # 백스페이스
Keys.ARROW_DOWN # 방향키 아래
Selenium에서 페이지가 로딩될 때까지 특정 요소가 "보일 때까지 기다리는" 코드
from selenium.webdriver.support import expected_conditions as ec
# 대기부 (검색 결과 리스트 로딩 대기)
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
visibility_of_element_located(locator)
: 조건이 참이 되는 시점까지 기다리는 함수
검색 결과 전체 리스트를 감싸고 있는 <ul> 태그가 "보이게 될 때까지" 기다리는 조건
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
검색결과 확인
place_list = driver.find_element(By.ID, "info.search.place.list")
shop_list = place_list.get_attribute("innerHTML")
get_attribute()
: Selenium의 WebElement
객체에서 HTML 속성(attribute)의 값을 가져오는 메서드
place_list
요소 내부에 있는 HTML 코드 전체(자식들 포함)를 문자열로 가져와서 shop_list
변수에 저장한다.
# 크롬드라이브를 종료합니다.
driver.quit()
# shop_list는 아래와 같은 코드에서 가져온 HTML 문자열입니다
return shop_list
디버깅과 예외 추적을 위한 코드:
except Exception as e:
- 위 코드(특히
try:
블록 안)에서 에러가 발생하면 이except
블록이 실행됩니다. Exception as e
는 발생한 오류 메시지를e
라는 변수에 저장합니다.
print(e)
- 예외 객체
e
를 출력합니다. - 콘솔이나 로그에 어떤 에러가 났는지 확인할 수 있도록 도와줍니다.
raise e
- 에러를 다시 바깥으로 던짐(재전파)합니다.
- 단순히 에러를 무시하지 않고, 이 함수가 호출된 상위 코드에게 "에러가 났다"고 알림으로 역할은 호출자에게 에러를 알리기 위한 재전파
의사코드:
# Selenium을 사용한 웹 크롤링 코드
# 카카오맵에서 "강남구 카페"를 검색하고, 결과 HTML 코드를 가져오는 함수
# 크롬 브라우저를 자동으로 열고 조작하기 위한 모듈
from selenium import webdriver
# 드라이버 서비스 설정용
from selenium.webdriver.chrome.service import Service
# 크롬 드라이버를 자동 설치해주는 라이브러리
from webdriver_manager.chrome import ChromeDriverManager
# 실행 중간에 잠깐 멈추는 기능
from time import sleep
# 요소를 찾을 때 id, class 등 기준을 정할 수 있게 함
from selenium.webdriver.common.by import By
# 키보드 입력 (예: 엔터키) 처리용
from selenium.webdriver.common.keys import Keys
# 최대 몇 초까지 기다릴지를 설정하는 기능
from selenium.webdriver.support.ui import WebDriverWait
# 기다리는 조건을 정의하는 도구
from selenium.webdriver.support import expected_conditions as ec
# 크롤링을 실행하는 함수 정의
def get_data_from_kakaomap():
try:
# 크롬 드라이버 자동 설치 및 실행을 위한 설정
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
# 크롬 브라우저로 카카오맵 웹사이트 열기
driver.get("https://map.kakao.com/")
# 최대 10초 동안 특정 요소(검색창)가 나타날 때까지 기다릴 수 있도록 설정
wait = WebDriverWait(driver, 10)
# id가 "search.keyword.query"인 검색창이 화면에 보일 때까지 대기
wait.until(ec.visibility_of_element_located((By.ID, "search.keyword.query")))
# 검색창 요소를 찾음
search_input = driver.find_element(By.ID, "search.keyword.query")
# 검색창에 "강남구 카페" 라는 텍스트 입력
search_input.send_keys("강남구 카페")
# 키보드의 Enter 키를 눌러 검색을 실행
search_input.send_keys(Keys.ENTER)
# 검색 결과 리스트가 나타날 때까지 다시 최대 10초 대기
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
# 검색 결과가 담긴 요소를 찾음
place_list = driver.find_element(By.ID, "info.search.place.list")
# 해당 요소의 HTML 코드를 가져옴 (여러 가게 목록이 포함됨)
shop_list = place_list.get_attribute("innerHTML")
# 브라우저 종료 (크롬 창 닫기)
driver.quit()
# 수집한 HTML 코드 반환
return shop_list
# 오류 발생 시 에러 메시지를 출력하고 예외로 넘김
except Exception as e:
print(e)
raise e
파트 | 설명 |
---|---|
함수 정의 | def get_data_from_kakaomap(): , def get_items(...): 와 같은 함수는 파이썬 함수 문법입니다. (기초 문법) |
Selenium 사용법 | 웹 브라우저 자동화를 위한 라이브러리입니다. 사용자가 마우스로 하는 일을 코드로 자동화함 |
BeautifulSoup 사용법 | 웹 페이지 HTML을 파싱해서 원하는 정보를 추출하는 데 사용됩니다. 즉, 데이터 "긁기" 역할 |
Jupyter notebook 실행:
jupyter notebook --no-browser --port=8888
연동된 Jupyter notebook에 kakaomap1.ipynb에 작성후 실행
import sys, os
sys.path.append(os.getcwd())
from selenium_crawler.kakaomap_scrap1 import get_data_from_kakaomap as get_data_v1
get_data_v1()
의사코드:
# 현재 작업 중인 디렉토리 경로를 시스템 경로(sys.path)에 추가하여
# 해당 폴더 안에 있는 모듈(파이썬 파일)을 불러올 수 있도록 설정함
import sys, os
sys.path.append(os.getcwd())
# 현재 작업 디렉토리 경로를 Python 모듈 검색 경로에 추가
# selenium_crawler라는 폴더(패키지) 안에 있는 kakaomap_scrap1.py 파일에서 get_data_from_kakaomap 함수를 불러오되, 이름을 get_data_v1으로 바꿔 사용하겠다는 뜻
from selenium_crawler.kakaomap_scrap1 import get_data_from_kakaomap as get_data_v1
# 위에서 불러온 함수(get_data_v1)를 실제로 실행함
# 강남구 카페 정보를 크롤링하고, 결과(html 또는 dict)를 반환함
get_data_v1()
kakaomap_scrap.py 수정
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
def get_data_from_kakaomap():
try:
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
# 웹사이트 열기
driver.get("https://map.kakao.com/")
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "search.keyword.query")))
# 검색창에 검색어 입력하기
search_input = driver.find_element(By.ID, "search.keyword.query")
# wait until the element is visible
search_input.send_keys("강남구 카페")
search_input.send_keys(Keys.ENTER)
wait = WebDriverWait(driver, 10)
wait.until(ec.element_to_be_clickable((By.ID, "info.search.place.more")))
driver.execute_script("""
var element = document.getElementById('dimmedLayer');
if (element) {
element.className = 'DimmedLayer HIDDEN';
}
""")
sleep(1)
show_more_btn = driver.find_element(By.ID, "info.search.place.more")
show_more_btn.click()
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.page")))
page_count = 0
items = []
while page_count <= 5:
if page_count != 0 and page_count % 5 == 0:
page_next_btn_id = "info.search.page.next"
next_btn = driver.find_element(By.ID, page_next_btn_id)
next_btn.click()
wait = WebDriverWait(driver, 10) wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
page_count += 1
page_num = page_count % 5 if page_count % 5 != 0 else 5
page_btn_id = f"info.search.page.no{page_num}"
next_btn = driver.find_element(By.ID, page_btn_id)
next_btn.click()
wait = WebDriverWait(driver, 10) wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
place_list = driver.find_element(By.ID, "info.search.place.list")
shop_list = place_list.get_attribute("innerHTML")
get_items(shop_list, items)
sleep(2)
# 검색 결과 확인
driver.quit()
return items
except Exception as e:
print(e)
raise e
def get_items(html: str, parsed_items: list):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
items = soup.select("li.PlaceItem.clickArea")
for item in items:
item_dict = {}
item_dict["name"] = item.find('span', {'data-id': 'screenOutName'}).text
item_dict["score"] = item.find('em', {'data-id': 'scoreNum'}).text
item_dict["address"] = item.find('p', {'data-id': 'address'}).text
item_dict["hour"] = item.find('a', {'data-id': 'periodTxt'}).text
parsed_items.append(item_dict)
return parsed_items
from time import sleep
# "더보기" 버튼이 로드될 때까지 대기
wait = WebDriverWait(driver, 10)
wait.until(ec.element_to_be_clickable((By.ID, "info.search.place.more")))
"더보기"
버튼이 완전히 로딩되고 클릭 가능할 때까지 기다림
검색 결과가 1페이지 이상일 경우, "더보기"를 눌러야 페이지네이션이 나타남
# 검은 화면(딤드 레이어)이 뜬 경우 강제로 숨김 (중요!)
driver.execute_script("""
var element = document.getElementById('dimmedLayer');
if (element) {
element.className = 'DimmedLayer HIDDEN';
}
""")
sleep(1)
var
는 Selenium이 JS를 DOM의 루트에서 실행할 때 더 호환성이 좋음
# "더보기" 버튼 클릭 → 페이지네이션 노출 유도
show_more_btn = driver.find_element(By.ID, "info.search.place.more")
show_more_btn.click()
클릭 메서드
# 페이지네이션 영역이 보일 때까지 대기
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.page")))
# 페이지 넘기며 반복 수집 (1~6페이지)
page_count = 0
items = []
while page_count <= 5:
if page_count != 0 and page_count % 5 == 0:
page_next_btn_id = "info.search.page.next"
next_btn = driver.find_element(By.ID, page_next_btn_id)
next_btn.click()
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
page_count += 1
page_num = page_count % 5 if page_count % 5 != 0 else 5
page_btn_id = f"info.search.page.no{page_num}"
next_btn = driver.find_element(By.ID, page_btn_id)
next_btn.click()
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
place_list = driver.find_element(By.ID, "info.search.place.list")
shop_list = place_list.get_attribute("innerHTML")
get_items(shop_list, items)
sleep(2)
검색 결과 페이지를 1페이지부터 6페이지까지 반복하면서 데이터를 수집하는 코드
반복 시작 전 초기화
page_count = 0 # 현재 몇 번째 페이지를 수집 중인지 추적하는 변수 (0부터 시작)
items = [] # 모든 카페 데이터를 담을 빈 리스트
while 반복문
while page_count <= 5: # 0~5까지 총 6페이지를 수집하겠다는 의미
5페이지 단위로 "다음" 버튼 클릭 처리
if page_count != 0 and page_count % 5 == 0:
5페이지마다 한 번씩 "다음" 버튼을 눌러야 함 카카오맵은 5페이지 단위로 페이지 그룹이 나뉘니까 필요. 5의 배수이므로 5마다 0이 나옵니다. 5,10,15,20...
page_next_btn_id = "info.search.page.next"
next_btn = driver.find_element(By.ID, page_next_btn_id)
next_btn.click() # "다음 >" 버튼 클릭
페이지 넘김을 위해 "다음" 버튼(id="info.search.page.next"
) 클릭
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
새 페이지가 로드될 때까지 기다림
page_count += 1 # 수집한 페이지 수를 1 증가
페이지 수 증가
현재 클릭할 페이지 버튼 ID 설정
page_num = page_count % 5 if page_count % 5 != 0 else 5
- 현재 페이지 번호에 해당하는 버튼은
info.search.page.no1
,no2
, ...,no5
- 예외 처리: 5의 배수일 때는
0
이 되므로5
로 바꿔줌
page_btn_id = f"info.search.page.no{page_num}"
# 예: info.search.page.no3
클릭할 페이지 번호의 ID 문자열 생성
next_btn = driver.find_element(By.ID, page_btn_id)
next_btn.click()
해당 페이지 번호 버튼을 찾아 클릭 (예: 2페이지 클릭)
wait = WebDriverWait(driver, 10)
wait.until(ec.visibility_of_element_located((By.ID, "info.search.place.list")))
페이지가 바뀐 뒤, 장소 목록이 다시 로딩될 때까지 기다림
place_list = driver.find_element(By.ID, "info.search.place.list")
shop_list = place_list.get_attribute("innerHTML")
<ul id="info.search.place.list">
내부의 HTML만 문자열로 가져옴
BeautifulSoup으로 HTML 파싱 후 items에 저장
get_items(shop_list, items)
get_items()
함수로 HTML 내부<li>
태그에서 이름, 평점, 주소 등 추출- 결과를
items
리스트에 누적 저장
페이지 간 sleep
sleep(2) # 너무 빠르게 넘어가면 서버 차단 가능성 있음 → 살짝 쉬어줌
def get_items(html: str, parsed_items: list):
from bs4 import BeautifulSoup
# HTML 파싱 준비
soup = BeautifulSoup(html, "html.parser")
# 가게 항목 리스트 선택
items = soup.select("li.PlaceItem.clickArea")
for item in items:
item_dict = {}
# 가게명
item_dict["name"] = item.find('span', {'data-id': 'screenOutName'}).text
# 평점
item_dict["score"] = item.find('em', {'data-id': 'scoreNum'}).text
# 주소
item_dict["address"] = item.find('p', {'data-id': 'address'}).text
# 영업시간
item_dict["hour"] = item.find('a', {'data-id': 'periodTxt'}).text
# 하나의 dict를 전체 리스트에 추가
parsed_items.append(item_dict)
return parsed_items
def get_items(html: str, parsed_items: list):
html
: 크롤링한ul
태그의 내부 HTML (innerHTML
) 문자열parsed_items
: 파싱한 결과를 저장할 리스트 (list[dict]
형태)- HTML에서 카페 이름, 평점, 주소, 영업시간 등을 추출해서
parsed_items
에 하나씩 딕셔너리로 추가함
BeautifulSoup로 HTML 파싱
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
html.parser
는 파이썬 내장 HTML 파서- HTML 문자열을 BeautifulSoup 객체로 바꿔서 태그 탐색이 가능하게 만듦
카페 항목 리스트 찾기
items = soup.select("li.PlaceItem.clickArea")
li.PlaceItem.clickArea
는:<li>
태그인데class="PlaceItem clickArea"
를 가진 요소
- 이 한 줄로 카카오맵에 표시된 카페 하나하나의 리스트 항목들을 전부 선택함
items
는 각 카페를 나타내는 BeautifulSoup 객체들의 리스트
각 카페 항목을 하나씩 순회
for item in items:
item_dict = {}
item
: 카페 하나에 해당하는<li>
요소item_dict
: 한 카페의 정보(name, score, address, hour)를 담을 딕셔너리
카페명 추출
item_dict["name"] = item.find("span", {"data-id": "screenOutName"}).text
<span data-id="screenOutName">카페이름</span>
에서 텍스트만 추출카페 이름
이 들어가는 위치
평점 추출
item_dict["score"] = item.find("em", {"data-id": "scoreNum"}).text
<em data-id="scoreNum">4.3</em>
등에서 평점 숫자만 가져옴
주소 추출
item_dict["address"] = item.find("p", {"data-id": "address"}).text
<p data-id="address">서울 강남구 ...</p>
에서 주소 텍스트 가져옴
영업시간 추출
item_dict["hour"] = item.find("a", {"data-id": "periodTxt"}).text
<a data-id="periodTxt">09:00 - 22:00</a>
처럼 영업시간 정보 추출
전체 결과 누적 저장
parsed_items.append(item_dict)
- 만들어진 딕셔너리를 최종 리스트(
parsed_items
)에 추가
결과 리턴
return parsed_items
모든 카페 정보를 담은 리스트를 반환
연동된 Jupyter notebook에 kakaomap.ipynb에 작성후 실행
import sys, os
sys.path.append(os.getcwd())
from selenium_crawler.kakaomap_scrap import get_data_from_kakaomap as get_data_v2
get_data_v2()